﻿/****************************************************************************************************
	Copyright (C) 2010 RapidWebDev Organization (http://rapidwebdev.org)
	Author: Eunge, Legal Name: Jian Liu, Email: eunge.liu@RapidWebDev.org

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 2 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ****************************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using BaoJianSoft.Common;
using BaoJianSoft.Common.Caching;
using BaoJianSoft.Common.Data;
using BaoJianSoft.Common.Validation;
using BaoJianSoft.Platform.Properties;
using AspNetMembership = System.Web.Security.Membership;
using System.Globalization;

namespace BaoJianSoft.Platform.Linq
{
    /// <summary>
	/// RapidWebDev Membership gives you a built-in way to validate and store user credentials which developed on ASP.NET Membership.
	/// The membership therefore helps you manage user authentication in your Web sites. 
	/// With extended from ASP.NET membership, you can configure the membership by ASP.NET Membreship Configuration as seeing the topic "Configuring an ASP.NET Application to Use Membership" in MSDN.
	/// The RapidWebDev membership model is NOT compatible to ASP.NET Membership Controls like Login Control, PasswordRecovery Control.
    /// </summary>
    public class MembershipApi : CachableApi, IMembershipApi
    {
        private IAuthenticationContext authenticationContext;
        private IOrganizationApi organizationApi;
		private MembershipDbProvider dbProvider;

        /// <summary>
		/// Construct membership API instance.
        /// </summary>
        /// <param name="authenticationContext"></param>
        /// <param name="organizationApi"></param>
        public MembershipApi(IAuthenticationContext authenticationContext, IOrganizationApi organizationApi)
        {
            this.authenticationContext = authenticationContext;
            this.organizationApi = organizationApi;
			this.dbProvider = new MembershipDbProvider(authenticationContext, organizationApi);
        }

        /// <summary>
        /// Save user object with specified password and password retrieval answer. 
		/// If it's used to update an existed user, the password allows to be null in the case that it doesn't need to change the password of the user.
		/// The argument "passwordAnswer" is to be validated depends on the configuration on ASP.NET Membership "RequiresQuestionAndAnswer".
        /// </summary>
        /// <param name="userObject">user business object</param>
		/// <param name="password">login password. If it's used to update an existed user, the password allows to be null in the case that it doesn't need to change the password of the user.</param>
		/// <param name="passwordAnswer">password retrieve answer. The argument "passwordAnswer" is to be validated depends on the configuration on ASP.NET Membership "RequiresQuestionAndAnswer".</param>
		/// <exception cref="ValidationException">Save user failed by various reasons implied in exception message.</exception>
        /// <exception cref="ArgumentException">The property userObject.Id is specified with an invalid value.</exception>
		/// <exception cref="ArgumentNullException">The argument userObject is null.</exception>
        public void Save(UserObject userObject, string password, string passwordAnswer)
        {
            Kit.NotNull(userObject, "userObject");

			try
			{
				if (userObject.UserId != Guid.Empty)
				{
					UserObject originalUserObject = this.Get(userObject.UserId);
					if (originalUserObject != null)
					{
						string originalUserNameCacheKey = this.FormatCacheKeyByUserName(originalUserObject.UserName);
						base.RemoveCache(originalUserNameCacheKey);
					}
				}

				this.dbProvider.Save(userObject, password, passwordAnswer);

				string userNameCacheKey = this.FormatCacheKeyByUserName(userObject.UserName);
				base.AddCache(userObject.UserId, userObject);
				base.AddCache(userNameCacheKey, userObject);
			}
			catch (ArgumentException)
			{
				throw;
			}
			catch (ValidationException)
			{
				throw;
			}
			catch (Exception exp)
			{
				Logger.Instance(this).Error(exp);
				throw;
			}
        }

        /// <summary>
        /// Resolve user elements from enumerable user ids.
        /// </summary>
        /// <param name="userIds"></param>
        /// <returns></returns>
        public IDictionary<Guid, UserObject> BulkGet(IEnumerable<Guid> userIds)
        {
            Dictionary<Guid, UserObject> returnedValue = new Dictionary<Guid, UserObject>();
			List<Guid> userIdList = userIds.ToList();
			foreach (Guid userId in userIdList)
            {
				UserObject userObject = base.GetCacheObject<UserObject>(userId);
				returnedValue.Add(userId, userObject);
            }

			List<Guid> userIdsNeededToLoadFromDb = returnedValue.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key).ToList();
			IDictionary<Guid, UserObject> usersLoadedFromDb = this.dbProvider.BulkGet(userIdsNeededToLoadFromDb);
			foreach (Guid userIdNeededToLoadFromDb in userIdsNeededToLoadFromDb)
			{
				if (usersLoadedFromDb.ContainsKey(userIdNeededToLoadFromDb) && usersLoadedFromDb[userIdNeededToLoadFromDb] != null)
				{
					UserObject userObjectLoadedFromDb = usersLoadedFromDb[userIdNeededToLoadFromDb];
					returnedValue[userIdNeededToLoadFromDb] = userObjectLoadedFromDb;

					base.AddCache(userObjectLoadedFromDb.UserId, userObjectLoadedFromDb);
					base.AddCache(this.FormatCacheKeyByUserName(userObjectLoadedFromDb.UserName), userObjectLoadedFromDb);
				}
			}

            return returnedValue;
        }

        /// <summary>
        /// Get user by user name.
        /// </summary>
        /// <param name="userName"></param>
        /// <returns></returns>
        public UserObject Get(string userName)
        {
			string userNameCacheKey = this.FormatCacheKeyByUserName(userName);
			UserObject userObject = base.GetCacheObject<UserObject>(userNameCacheKey);
			if (userObject != null) return userObject;

			userObject = this.dbProvider.Get(userName);
			if (userObject != null)
			{
				base.AddCache(userObject.UserId, userObject);
				base.AddCache(userNameCacheKey, userObject);
			}

			return userObject;
        }

        /// <summary>
        /// Get user object by user id.
        /// </summary>
        /// <param name="userId">user id</param>
        /// <returns></returns>
        public UserObject Get(Guid userId)
        {
			UserObject userObject = base.GetCacheObject<UserObject>(userId);
			if (userObject != null) return userObject;

			userObject = this.dbProvider.Get(userId);
			if (userObject != null)
			{
				base.AddCache(userObject.UserId, userObject);

				string userNameCacheKey = this.FormatCacheKeyByUserName(userObject.UserName);
				base.AddCache(userNameCacheKey, userObject);
			}

			return userObject;
        }

        /// <summary>
		/// Validate user is authenticated to application. Both invalid credential and unauthenticated organizations will be failed logon.
        /// </summary>
        /// <param name="username">user name</param>
        /// <param name="password">password</param>
        public LoginResults Login(string username, string password)
        {
            try
            {
                Guid applicationId = this.authenticationContext.ApplicationId;

                bool isvalid = AspNetMembership.ValidateUser(username, password);
                if (!isvalid) return LoginResults.InvalidCredential;

                using (MembershipDataContext ctx = DataContextFactory.Create<MembershipDataContext>())
                {
                    UserObject userObject = this.Get(username);
                    if (!userObject.IsApproved)
                        return LoginResults.InvalidCredential;

                    OrganizationObject organizationObject = this.organizationApi.GetOrganization(userObject.OrganizationId);

                    if (organizationObject.Status != OrganizationStatus.Enabled)
                        return LoginResults.InvalidOrganization;

                    return LoginResults.Successful;
                }
            }
            catch (Exception exp)
            {
                Logger.Instance(this).Error(exp);
                throw;
            }
        }

        /// <summary>
        /// Change password of specified user. 
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="oldPassword"></param>
        /// <param name="newPassword"></param>
        /// <returns>returns true if operation successfully.</returns>
        public bool ChangePassword(Guid userId, string oldPassword, string newPassword)
        {
            Kit.NotNull(newPassword, "newPassword");

            if (newPassword.Length < AspNetMembership.MinRequiredPasswordLength)
            {
                throw new ValidationException(string.Format(Resources.PasswordLessThanMinLength, AspNetMembership.MinRequiredPasswordLength));
            }

            if (!Kit.IsEmpty(AspNetMembership.PasswordStrengthRegularExpression))
            {
                Regex regex = new Regex(AspNetMembership.PasswordStrengthRegularExpression, RegexOptions.Compiled);
                if (!regex.IsMatch(newPassword))
					throw new ValidationException(Resources.InvalidPasswordFormat);
            }

            try
            {
                var membershipUser = AspNetMembership.GetUser(userId, true);
                return membershipUser.ChangePassword(oldPassword, newPassword);
            }
            catch (Exception exp)
            {
                Logger.Instance(this).Error(exp);
                throw;
            }
        }

		private string FormatCacheKeyByUserName(string userName)
		{
			return string.Format(CultureInfo.InvariantCulture, "{0}@{1}", userName, authenticationContext.ApplicationId);
		}
    }
}
